package ligne2vie.topos.server.service;

import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import ligne2vie.topos.client.service.SummitService;
import ligne2vie.topos.server.bo.ActionHistory;
import ligne2vie.topos.server.bo.ActionHistory.ActionType;
import ligne2vie.topos.server.bo.EntityState;
import ligne2vie.topos.server.bo.Summit;
import ligne2vie.topos.server.bo.geocell.GeocellManager;
import ligne2vie.topos.server.bo.geocell.GeocellQuery;
import ligne2vie.topos.server.bo.geocell.Point;
import ligne2vie.topos.shared.bo.TMapPoint;
import ligne2vie.topos.shared.bo.TSummit;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

/**
 * Implementation of {@link SummitService}.
 * 
 * @author thuguerre
 * @since 1.0.0
 */
public class SummitServiceImpl extends AbstractRemoteService implements SummitService {

	/**
	 * {@link SummitServiceImpl}'s logger.
	 */
	private static final Logger LOGGER = Logger.getLogger(SummitServiceImpl.class.getName());

	/**
	 * Sets the max number of summits to find when geo-querying summits.
	 * 
	 * @since 1.1.0
	 */
	private static final int MAX_SUMMITS_TO_FIND = 100;

	/**
	 * User Service to use user authentication when required.
	 */
	private static final UserServiceImpl userService = new UserServiceImpl();

	/**
	 * Default serial UID.
	 */
	private static final long serialVersionUID = -8590696276845812878L;

	/**
	 * Default constructor.
	 */
	public SummitServiceImpl() {
	}

	/**
	 * The method {@link #_getSummit(long)} returns the {@link Summit} corresponding to the received identifier.
	 * 
	 * @param summitId
	 *            Identifier of the summit to get.
	 * @return Corresponding summit.
	 */
	protected Summit _getSummit(long summitId, boolean noRemoved) {

		LOGGER.fine("Getting summit [" + summitId + "].");

		PersistenceManager pm = _getPM();
		Key k = KeyFactory.createKey(Summit.class.getSimpleName(), summitId);

		try {

			Summit summit = pm.getObjectById(Summit.class, k);

			if (noRemoved && summit.getState().equals(EntityState.REMOVED)) {
				throw new RuntimeException("The summit [" + summitId + "] has been removed.");
			}

			return pm.detachCopy(summit);

		} finally {
			pm.close();
		}
	}

	/**
	 * Convenience method returning an action history of the desired type, filled with current date and current user.
	 * 
	 * @param type
	 *            Type of the action.
	 * 
	 * @return Action history which can be directly added to the entity.
	 */
	protected ActionHistory _createActionHistory(ActionType type) {

		return new ActionHistory(new Date(), type, userService.getCurrentUser().getEmail());
	}

	/**
	 * {@inheritDoc}
	 */
	protected Summit _createSummit(String name, int altitude, double latitude, double longitude, String description) {

		LOGGER.fine("Getting summit [" + name + "].");

		// --- verifying parameters

		TMapPoint.areParametersValid(name, altitude, latitude, longitude, description);

		// --- creating BO object

		Summit summit = new Summit(name, altitude, latitude, longitude, description);
		summit.add(_createActionHistory(ActionType.SUMMIT_CREATE));
		summit.setGeoCells();

		// --- persisting BO object into database

		persist(summit);

		// --- returning created summit

		LOGGER.fine("Returning summit...");
		return summit;
	}

	/**
	 * {@inheritDoc}
	 */
	public TSummit createSummit(String name, int altitude, double latitude, double longitude, String description) {

		_userShouldBeAuthenticated();

		return this._createSummit(name, altitude, latitude, longitude, description).getTSummit();
	}

	/**
	 * {@inheritDoc}
	 */
	protected Summit _editSummit(long summitId, String name, int altitude, double latitude, double longitude,
			String description) {

		// --- verifying parameters

		TMapPoint.areParametersValid(name, altitude, latitude, longitude, description);

		LOGGER.fine("Starting editing summit...");
		Summit summit = _getSummit(summitId, true);

		// --- editing BO object

		summit.setName(name);
		summit.setAltitude(altitude);
		summit.setLatitude(latitude);
		summit.setLongitude(longitude);
		summit.setDescription(description);
		summit.add(_createActionHistory(ActionType.SUMMIT_EDIT));
		summit.setGeoCells();

		// --- persisting BO object into database

		persist(summit);

		// --- returning created summit

		LOGGER.fine("Returning summit...");
		return summit;
	}

	/**
	 * {@inheritDoc}
	 */
	public TSummit editSummit(long summitId, String name, int altitude, double latitude, double longitude,
			String description) {

		_userShouldBeAuthenticated();

		return _editSummit(summitId, name, altitude, latitude, longitude, description).getTSummit();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<TSummit> findSummitsAround(double latitude, double longitude, double radius) {

		LOGGER.fine("Finding summits at less then " + radius + " km from [" + latitude + "," + longitude + "]...");

		PersistenceManager pm = _getPM();
		try {

			// --- using GeoModel project to query the summits near the given position

			List<Summit> boResult = GeocellManager.proximityFetch(new Point(latitude, longitude), MAX_SUMMITS_TO_FIND,
					radius * 1000, Summit.class, new GeocellQuery(null, null, null), _getPM());

			// --- converting and returning the result

			return Conversion.toTSummitList(boResult);

		} finally {
			pm.close();
		}
	}

	public List<Summit> _getAllSummits(boolean excludeRemoved) {

		LOGGER.fine("Getting all summits.");

		PersistenceManager pm = _getPM();
		Query query = pm.newQuery(Summit.class);

		if (excludeRemoved) {

			// filtering summits which have been removed.

			query.setFilter("state != stateParam");
			query.declareParameters("EntityState stateParam");
		}

		try {

			@SuppressWarnings("unchecked")
			List<Summit> boResult = excludeRemoved ? (List<Summit>) query.execute(EntityState.REMOVED)
					: (List<Summit>) query.execute();
			LOGGER.fine(boResult.size() + " summits found.");

			return boResult;

		} finally {
			query.closeAll();
			pm.close();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public List<TSummit> getAllSummits() {
		return Conversion.toTSummitList(_getAllSummits(true));
	}

	/**
	 * {@inheritDoc}
	 */
	public TSummit getSummit(long summitId) {
		return _getSummit(summitId, true).getTSummit();
	}

	/**
	 * {@inheritDoc}
	 */
	protected void _removeSummit(long summitId) {

		LOGGER.fine("Getting summit to remove.");

		Summit summit = _getSummit(summitId, true);

		// --- editing BO object

		summit.setState(EntityState.REMOVED);
		summit.add(_createActionHistory(ActionType.SUMMIT_REMOVE));

		// --- persisting BO object into database

		persist(summit);
	}

	/**
	 * {@inheritDoc}
	 */
	public void removeSummit(long summitId) {

		_userShouldBeAuthenticated();
		_removeSummit(summitId);
	}

	/**
	 * For certain actions, such as {@link #createSummit(String, int, double, double, String)}, the user should be
	 * authenticated to perform it. This method throws an exception if it is not the case. It should be called at every
	 * start of requiring action.
	 */
	protected void _userShouldBeAuthenticated() {

		if (!userService.isUserLoggedIn()) {
			throw new RuntimeException("User should be authenticated.");
		}
	}
}
